-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

-- Description: 升级对象
local skynet = require 'skynet'
local log = require 'mc.logging'
local MCU_ENUMS = require 'mcu.enum.mcu_enums'
local utils = require 'mc.utils'
local utils_core = require 'utils.core'
local cmn = require 'common'
local file_sec = require 'utils.file'
local chip_lock_singleton = require 'chip_lock'

local RET_OK = 0
local RET_ERR = -1
local ROOT_USER_GID = 0 -- root用户组id 0
local OPERATOR_GID = 200 -- 操作者用户组id 200
local MCU_UPGRADE_TRY_COUNT = 3

local upgrade_object = {}
upgrade_object.__index = upgrade_object


function upgrade_object.new(mcu_obj, system_id, power_state)
    local obj = {}
    obj.system_id = system_id
    obj.power_state = power_state
    obj.id = mcu_obj.mcu.Id            -- string
    obj.chip = mcu_obj.mcu.RefChip        -- refChip
    obj.lock_chip = mcu_obj.mcu.LockChip
    obj.interface_type = mcu_obj.mcu.Protocol    -- string
    -- 成员: obj.major_version             -- number
    -- 成员: obj.minor_version             -- number
    -- 成员: obj.revision                  -- number
    -- 成员: obj.ref_mcu                   -- mcu_object
    -- 成员: obj.ref_sub_comp              -- sub_comp_object

    obj.device_name = mcu_obj.device_name
    obj.name = mcu_obj.name

    return setmetatable(obj, upgrade_object)
end

function upgrade_object:get_system_id()
    return self.system_id
end

function upgrade_object:set_ref_mcu(mcu)
    self.ref_mcu = mcu
end

function upgrade_object:get_ref_mcu()
    return self.ref_mcu
end

function upgrade_object:set_ref_subcomp(subcomp)
    self.ref_sub_comp = subcomp
end

function upgrade_object:get_ref_subcomp()
    return self.ref_sub_comp
end

--- @function 获取升级对象的升级文件路径
function upgrade_object:get_file_path(dir)
    local obj = self:get_ref_mcu()
    if not obj then
        log:error('get ref mcu obj failed')
        return nil, nil
    end

    -- 根据子件type number转type string
    local file_path
    local file_size

    if self.interface_type == MCU_ENUMS.SMC_CHANNEL then
        local subcomp = self:get_ref_subcomp()
        if not subcomp then
            log:error('get ref subcomp failed')
            return nil, nil
        end
        -- 升级前需要更新固件的信息
        subcomp:update_detail_info()
        local type_str = MCU_ENUMS.SUB_COMPONENT_TYPE_TABLE[subcomp.Type]
        local suffix = MCU_ENUMS.SUB_COMPONENT_BIN_SUFFIX_TABLE[subcomp.Type]

        -- 子件类型_厂商ID_型号ID_Index.文件后缀
        file_path = string.format("%s%s_%02X_%02X_%02X.%s", dir, type_str, subcomp.Vendor, subcomp.SKU,
            subcomp.No, suffix)
    else
        local vendor_id = obj.interface:get_vendor_id(self:get_ref_subcomp())
        if not vendor_id then
            log:error("get vendor_id fail")
            return nil, nil
        end
        file_path = string.format("%smcu%04x.bin", dir, vendor_id)
    end

    -- 文件权限修改
    utils_core.chown_s(file_path, ROOT_USER_GID, OPERATOR_GID)
    utils_core.chmod_s(file_path, utils.S_IRUSR | utils.S_IRGRP)

    -- 检查文件是否存在
    local ret = file_sec.check_real_path_s(file_path)
    if ret ~= RET_OK then
        log:error("file-%s not exist, err:%s", file_path, ret)
        return nil, nil
    end

    -- 获取文件大小
    local file_stat = utils_core.stat_s(file_path)
    file_size = file_stat.st_size
    return file_path, file_size
end

--- @function 当前是否可升级
function upgrade_object:is_upgradeable(lib_size, bin_size)
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    if self.interface_type == MCU_ENUMS.SMC_CHANNEL then
        local lib_max_size = obj:get_lib_max_size()
        local bin_max_size = obj:get_bin_max_size()
        if lib_max_size < lib_size or bin_max_size < bin_size then
            log:error("MCU buffer cannot match lib_size %s or bin_size %s", lib_size, bin_size)
            return RET_ERR
        end
    end

    return RET_OK
end

--- @function 初始化升级通道
function upgrade_object:init_upgrade(lib_size, bin_size, fw_info)
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Init mcu upgrade channel",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local ok = obj.interface:init_upgrade(self:get_ref_subcomp(), lib_size, bin_size)
    if not ok then
        log:error("init_upgrade failed")
        return RET_ERR
    end
    return RET_OK
end


local function get_send_params(vendor, board_type, fw_type)
    log:notice('[McuUpgrade]vendor:%s,BoardType:%s,fw_type:%s', vendor, board_type, fw_type)
    -- 默认的MCU升级为写入内存,每写入2048btype等待10ms,每次写入96个字节
    local send_params_list = {}
    send_params_list.delay = 1
    send_params_list.subsection_size = 2048
    send_params_list.smc_data_len = MCU_ENUMS.SMC_FILE_DATA_LEN

    if vendor and vendor == 1 and fw_type and fw_type == 0 and board_type ~= 'ACU' then
        -- 写入flash为等待4s, 硬件建议每2304KB等待
        -- MCU:每次传包有效数据长度是96Byte，设置为2304Byte为96Byte的倍数，方便MCU将传入的数据写入内部flash
        send_params_list.delay = 400
        send_params_list.subsection_size = 2304
    end
    if vendor and vendor == 0 and fw_type == 0 and board_type == 'BCU' then
        -- ST基础板的MCU升级硬件建议等待0.5s
        send_params_list.delay = 50
        send_params_list.subsection_size = 2304
    end
    -- NPU模组的MCU升级每次写入240个字节
    if board_type == 'ACU' then
        send_params_list.smc_data_len = MCU_ENUMS.NPU_SMC_FILE_DATA_LEN
    end

    return send_params_list
end

--- @function 发送文件给MCU
--- @param file_name string 升级文件路径
--- @return number RET_OK和RET_ERR
function upgrade_object:send_upgrade_file(file_name, upg_prog_cb)
    local file = file_sec.open_s(file_name, "rb")
    if not file then
        return RET_ERR
    end
    local data = utils.close(file, pcall(file.read, file, 'a'))

    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end
    local sub_comp = self:get_ref_subcomp()
    local vendor, fw_type, delay
    if sub_comp then
        vendor = sub_comp.Vendor
        fw_type = sub_comp.Type
        delay = sub_comp.Delay -- 新MCU支持获取建议等待时间, 单位为100ms
    end
    local board_type = obj:get_board_type()
    local send_params_list = get_send_params(vendor, board_type, fw_type)
    -- 如果MCU支持获取等待时间, 则替换等待时间为MCU获取,0和255为无效值
    if delay and delay > 0 and delay ~= 255 and board_type == 'BCU' then
        send_params_list.delay = delay * 10
    end
    local ok = obj.interface:send_upgrade_file(self:get_ref_subcomp(), data, send_params_list, upg_prog_cb,
        board_type == 'ACU')
    if not ok then
        log:error("send_upgrade_file failed")
        return RET_ERR
    end
    return RET_OK
end

--- @function 等待MCU的状态转换为空闲状态，查询到MCU处于空闲状态或者等待超时，则退出
--- @param timeout_sec number 等待超时时间(单位：秒)
--- @return number RET_OK和RET_ERR
function upgrade_object:wait_mcu_to_available(timeout_sec)
    local time = 1

    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local status, smc_channel
    while time < timeout_sec do
        status = obj.interface:query_upgrade_status()
        smc_channel = obj.interface.type == MCU_ENUMS.SMC_CHANNEL
        if status and smc_channel and status ~= MCU_ENUMS.SMC_CALL_STATUS_CODE.BUSY then
            log:notice('query SMC upgrade status is %s', status)
            return status
        end

        if status == MCU_ENUMS.MCU_UPGRADE_STATUS.FAIL then
            log:error("upgrade status failed. status is %s", status)
            return RET_ERR
        end

        if status == MCU_ENUMS.MCU_UPGRADE_STATUS.IDLE then
            break
        end

        cmn.skynet.sleep(100)
        time = time + 1
    end
    -- 响应超时应该返回升级失败，触发重试
    return time == timeout_sec and RET_ERR or RET_OK
end

--- @function 发送启动升级命令
--- @return number RET_OK和RET_ERR
function upgrade_object:start_upgrade()
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local ok = obj.interface:start_upgrade()
    if not ok then
        log:error("start_upgrade failed")
        return RET_ERR
    end
    return RET_OK
end

--- @function 发送使升级生效命令
function upgrade_object:take_upgrade_effect()
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local ok = obj.interface:take_upgrade_effect()
    if not ok then
        log:error("take_upgrade_effect failed")
        return RET_ERR
    end
    return RET_OK
end

--- @function 查询生效模式
function upgrade_object:query_active_mode()
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    if obj:get_board_type() == 'ACU' then
        return MCU_ENUMS.ACTIVE_IMMEDIATELY
    end

    local ok, active_mode = pcall(function ()
        return obj.interface:query_active_mode()
    end)
    if not ok then
        log:notice("query active mode failed, err: %s", active_mode)
        return MCU_ENUMS.ACTIVE_IMMEDIATELY
    end

    log:notice("query active mode successfully, active mode is %s", active_mode)
    return active_mode
end

--- @function 根据生效模式执行生效动作
function upgrade_object:exec_valid_action_with_condition()
    -- 查询生效模式，立即生效模式直接执行生效动作
    local active_mode = self:query_active_mode()
    if active_mode == MCU_ENUMS.ACTIVE_IMMEDIATELY then
        self:exec_valid_action()
        return
    end
end

--- @function 执行生效动作
function upgrade_object:exec_valid_action()
    local obj = self:get_ref_mcu()
    if not obj then
        return
    end

    if obj:get_board_type() == 'ACU' then
        return
    end

    local ok, rsp = pcall(function ()
        obj.interface:exec_valid_action()
    end)
    if not ok then
        log:error("execute valid action failed, error is %s", rsp)
        return
    end

    log:notice("execute valid action successfully!")
end

--- @function 实际失败, 执行关闭
function upgrade_object:upgrade_close()
    log:notice('Upgrade failed, start close step')
    local ok, obj = pcall(function ()
        return self:get_ref_mcu()
    end)

    if not ok or not obj then
        return RET_ERR
    end

    if self.interface_type == MCU_ENUMS.SMC_CHANNEL then
        local deinit_res
        ok, deinit_res = pcall(function ()
            return obj.interface:take_upgrade_effect()
        end)

        if not ok or not deinit_res then
            log:error("close step failed")
        end
    end
    log:notice("close successfully")
end

--- @function 获取mcu版本，超时时间120s
--- @param timeout_sec number 等待超时时间(单位：秒)
--- @return any mcu主版本或者nil
--- @return any mcu备版本或者nil
--- @return any mcu修订版本或者nil
function upgrade_object:wait_mcu_to_get_version(timeout_sec)
    local time = 1

    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local major_version, minor_version, revision
    while time < timeout_sec do
        major_version, minor_version, revision = obj.interface:get_version(self:get_ref_subcomp())
        if major_version then
            return major_version, minor_version, revision
        end
        log:debug("get mcu version faild")
        cmn.skynet.sleep(100)
        time = time + 1
    end
    return nil
end

-- 设置firmware对象版本
function upgrade_object:update_firmware_version()
    local major_version, minor_version, revision = self:wait_mcu_to_get_version(MCU_ENUMS.MCU_UPGRADE_DELAY.T1)
    if not major_version then
        log:error("set mcu majorversion and minorversion failed")
        return
    end

    if major_version then
        self.major_version = major_version
    end

    if minor_version then
        self.minor_version = minor_version
    end

    if revision then
        self.revision = revision
    end
end

function upgrade_object:reset_mcu_before_upgrade(try_count)
    if try_count ~= MCU_UPGRADE_TRY_COUNT then
        return RET_OK
    end
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end
    local board_type = obj:get_board_type()
    if board_type ~= 'ACU' then
        return RET_OK
    end
    self:start_reset_mcu()
    --等待mcu启动成功，时长10s
    skynet.sleep(1000)
    return self:wait_mcu_to_available(MCU_ENUMS.MCU_UPGRADE_DELAY.T180)
end

function upgrade_object:upgrade_process(dir, fw_info, process_callback)
    for index = 1, MCU_UPGRADE_TRY_COUNT do
        local ret = self:retry_upgrade(dir, fw_info, process_callback, index)
        log:notice('upgrade result is %s, upgrade %s', ret, MCU_ENUMS.RET_CODE_REASON[ret] or 'unknown')
        if index ~= 1 then
            log:notice('retry res is %s, upgrade %s', ret, MCU_ENUMS.RET_CODE_REASON[ret] or 'unknown')
        end

        skynet.sleep(10) -- close之后会触发固件信息更新

        -- 只对预期内的值做返回
        if ret == MCU_ENUMS.MCU_UPGRADE_STATUS.IDLE or
            ret == MCU_ENUMS.MCU_UPGRADE_STATUS.INPROCESS or
            ret == MCU_ENUMS.MCU_UPGRADE_STATUS.LOW_VERSION then
            return ret
        end
    end
    return RET_ERR
end

function upgrade_object:log_err_and_close(err, ret)
    log:error(err)
    self:upgrade_close()
    return ret
end

function upgrade_object:upgrade_effect_process(fw_info)
    --发送使升级命令生效
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Send upgrade take effect command",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    local ret = self:take_upgrade_effect()
    if ret == RET_ERR then
        return ret
    end
    cmn.skynet.sleep(100) --延时1s, 等待smc状态转换
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Query active mode",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    --查询生效模式并执行
    self:exec_valid_action_with_condition()

    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Send mcu upgrade version",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    --等待读取版本信息并刷入upgrade_object
    self:update_firmware_version()
    return ret
end

function upgrade_object:process_active_error(err, ret)
    local obj = self:get_ref_mcu()
    local board_type = obj:get_board_type()
    if board_type == 'ACU' then
        log:error(err)
        return ret
    end
    return self:log_err_and_close(err, ret)
end

--- @function 升级流程
-- 无论升级成功还是失败，都要执行关闭操作
function upgrade_object:retry_upgrade(dir, fw_info, process_callback, try_count)
    local ret = self:reset_mcu_before_upgrade(try_count)
    if ret ~= RET_OK then
        return self:log_err_and_close('reset mcu failed', ret)
    end
    local lib_size = 0

    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Get mcu upgrade file path",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    -- 获取升级对象的升级文件路径
    local file_path, bin_size = self:get_file_path(dir)
    if not file_path then
        log:error("failed to get MCU upgrade file_path")
        return RET_ERR
    end
    self.path = string.sub(file_path, #dir + 1, -1):gsub("%.bin$", "")

    -- 判断可升级状态
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Get upgrade support status",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    if self:is_upgradeable(lib_size, bin_size) == RET_ERR then
        log:error("not support upgrade now")
        return RET_ERR
    end

    -- 初始化通道
    ret = self:init_upgrade(lib_size, bin_size, fw_info)
    if ret == RET_ERR then
        return self:log_err_and_close("fail to init upgrade", RET_ERR)
    end

    --MCU分片升级，防止第一帧数据丢失，添加2s延时
    skynet.sleep(200)
    -- 发送升级文件
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Send upgrade file",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    ret = self:send_upgrade_file(file_path, process_callback)
    if ret == RET_ERR then
        return self:log_err_and_close('fail to upload file to mcu', RET_ERR)
    end

    -- 等待MCU升级状态变为空闲,等待最多1000 * 10ms(10秒),否则超时返回
    skynet.sleep(1000)
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Wait upgrade status",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    ret = self:wait_mcu_to_available(MCU_ENUMS.MCU_UPGRADE_DELAY.T0)
    if ret ~= RET_OK then
        return self:log_err_and_close('upgrade failed', ret)
    end

    -- 发送启动升级命令
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Send mcu upgrade init command",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    ret = self:start_upgrade()
    if ret == RET_ERR then
        return self:log_err_and_close('fail to send upgrade initate command', RET_ERR)
    end
    cmn.skynet.sleep(100) -- 延时1s，等待smc状态转换
    log:notice("---MCU INDEX: %s, ---MCU ID:%s, ---Wait upgrade finish status",
        fw_info.fw_index, fw_info.fw_obj and fw_info.fw_obj.id or 'fw_obj is nil')
    -- 查询升级是否完成
    ret = self:wait_mcu_to_available(MCU_ENUMS.MCU_UPGRADE_DELAY.T1)
    if ret ~= RET_OK then
        return self:log_err_and_close('upgrade failed.', ret)
    end
    -- 发送使升级生效命令
    ret = self:upgrade_effect_process(fw_info)
    if ret == RET_ERR then
        return self:process_active_error('fail to send upgrade takeeffect command', RET_ERR)
    end
    return RET_OK
end

function upgrade_object:chip_lock_supported()
    if not self.lock_chip.SetLockStatus then
        return false
    end
    return true
end

function upgrade_object:chip_lock(ctx, lock_time)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():lock(self.lock_chip, ctx, lock_time)
    end)
end

function upgrade_object:chip_unlock(ctx)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():unlock(self.lock_chip, ctx)
    end)
end

---@function 发送启动升级命令
---@return number RET_OK和RET_ERR
function upgrade_object:start_reset_mcu()
    local obj = self:get_ref_mcu()
    if not obj then
        return RET_ERR
    end

    local ok = obj.interface:start_reset_mcu()
    if not ok then
        log:error("start reset mcu failed")
        return RET_ERR
    end
    return RET_OK
end

return upgrade_object